/**
 * Provides remote asynchronous service for communicating with the sensorbase and telemetry 
 * analysis service hosts.
 */
package org.hackystat.ui.projectviewer.server;

import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import javax.xml.datatype.XMLGregorianCalendar;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import org.hackystat.dailyprojectdata.client.DailyProjectDataClient;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.client.SensorBaseClientException;
import org.hackystat.sensorbase.resource.projects.jaxb.Invitations;
import org.hackystat.sensorbase.resource.projects.jaxb.Members;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.projects.jaxb.ProjectIndex;
import org.hackystat.sensorbase.resource.projects.jaxb.ProjectRef;
import org.hackystat.sensorbase.resource.projects.jaxb.Properties;
import org.hackystat.sensorbase.resource.projects.jaxb.Property;
import org.hackystat.sensorbase.resource.projects.jaxb.UriPatterns;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.hackystat.ui.projectviewer.client.common.ProjectParcel;
import org.hackystat.ui.projectviewer.client.common.RPCRemoteService;
import org.hackystat.ui.projectviewer.client.common.RPCException;
import org.hackystat.utilities.tstamp.Tstamp;

/**
 * Provides remote asynchronous service for communicating with the sensorbase and dailyprojectdata 
 * analysis service hosts.
 * @author David J. Nickles
 */
public class RPCServiceImpl extends RemoteServiceServlet implements RPCRemoteService {

  /* Class variables for serialized object id */
  private static final long serialVersionUID = 11121936L;
  /** The sensor properties instance. */
  private org.hackystat.ui.projectviewer.server.Properties properties; 
  /** {@literal} USERLABEL a recurring string contant. */  
  private static final String USERLABEL = ", User: ";
  
  /**
   * Returns true if the passed host is a DailyProjectData Analysis service host.
   * 
   * @param host the URI for the DailyProjectData Host.
   * @return Boolean indicating if the host is a valid host.
   */
  public Boolean isDailyProjectDataHost(String host) {
    log("PV checking isDailyProjectDataHost " + host + '\n');
    return DailyProjectDataClient.isHost(host);
  }
  
  /**
   * Returns true if the passed host is a Sensorbase service host.
   * 
   * @param host the URI for the SensorBase Host.
   * @return Boolean indicating if the host is a valid host.
   */
  public Boolean isSensorBaseHost(String host) {
    log("PV checking isSensorBaseHost " + host + '\n');
    return SensorBaseClient.isHost(host);
  }

  /**
   * Returns true if the user and password is registered as a user with the sensorbase host.
   * 
   * @param host the URI for the SensorBase
   * @param user user to authentication
   * @param password user password for authentication
   * @return Boolean indicating if the user and password are valid in the sensorbase
   */
  public Boolean isRegistered(String host, String user, String password) {
    log("PV checking isRegistered with sensorbase, user: " + user + '\n');
    return SensorBaseClient.isRegistered(host, user, password);
  }

  /**
   * Registers the user with this host.
   * 
   * @param host the URI for the SensorBase
   * @param user user to authentication
   * @return Boolean indicating if the user and password are valid in the sensorbase
   */
  public Boolean registerUser(String host, String user) {
    log("PV register, user: " + user + '\n');
    Boolean result = Boolean.TRUE;
    try {
      SensorBaseClient.registerUser(host, user);
    }
    catch (SensorBaseClientException e) {
      result = Boolean.FALSE;
      log("PV register: " + e.getMessage());
    }
    return result;
  }
  
  /**
   * Returns the attributes set in the Manifest file.
   * 
   * @return a list of strings of attribute values from the Manifest file.
   */
  public ArrayList<String> getManifest() {
    ArrayList<String> result = new ArrayList<String>();
    try {
      URL manifestURL = getServletContext().getResource("/META-INF/MANIFEST.MF");
      if (manifestURL == null) {
        log("Unable to get Manifest file.\n");
        return result;
      }
      Manifest mf = new Manifest(manifestURL.openStream());
      Attributes attr = mf.getMainAttributes();
      String title = attr.getValue("Implementation-Title");
      if (title == null) {
        log("Unable to get 'Implementation-Title' attribute from the Manifest.\n");
        title = "Hackystat Developer Widget";
      }
      result.add(title);
      String version = attr.getValue("Implementation-Version");
      if (version == null) {
        log("Unable to get 'Implementation-Version' attribute from the Manifest.\n");
        version = "Development";
      }
      result.add(version);
      String vendor = attr.getValue("Implementation-Vendor");
      if (vendor == null) {
        log("Unable to get 'Implementation-Version' attribute from the Manifest.\n");
        vendor = "CSDL @ UH";
      }
      result.add(vendor);
    } 
    catch (Exception e) {
      log("PV version excpetion thrown " + e.getMessage() + '\n');
    }
    log("PV checking manifest, version=" + result + '\n');
    return result; 
  }
  
  /**
   * This method retrieves the host uris for the Sensorbase and DailyProjectData services from the
   * Project Viewer properties file.
   * 
   * @return An arraylist of the host uris for the Sensorbase and DailyProjectData services from
   * the Project Viewer properties file.
   */
  public ArrayList<String> getHosts() {
    log("Entering method: PV props\n");
    ArrayList<String> result = new ArrayList<String>();
    try {
      this.properties = new org.hackystat.ui.projectviewer.server.Properties();
      if (this.properties == null) {
        log("PV props: Unable to get PV properties file.\n");
        return result;
      }
      result.add(this.properties.getDailyProjectDataHost());
      result.add(this.properties.getSensorbaseHost());
      if (result == null) {
        log("PV props: Unable to get hosts attributes from the properties file.\n");
        return result;
      }
    } 
    catch (Exception e) {
      log("PV props exception thrown " + e.getMessage() + '\n');
    }
    log("PV props success: DailyProjectData host=" + result.get(0) + '\n');
    log("PV props success: Sensorbase host=" + result.get(1) + '\n');
    return result; 
  }
  
  /**
   * Zeros the hour.minute.second.millisecond for this timestamp.
   * @param cal timestamp as XMLGregorianCalendar object
   * @return the quantized timestamp as an XMLGregorianCalendar object
   */
  private XMLGregorianCalendar quantize(XMLGregorianCalendar cal) {
    cal.setHour(0);
    cal.setMinute(0);
    cal.setSecond(0);
    cal.setMillisecond(0);
    return cal;
  }
  
  /**
   * Computes the first day of the given month.
   * Zeros the hour.minute.second.millisecond for this timestamp.
   * @param year the number of the year as an integer
   * @param month the number of the month as an integer
   * @return the first of the month timestamp as an XMLGregorianCalendar object
   */
  private XMLGregorianCalendar getFirstOfMonth(int year, int month) {
    XMLGregorianCalendar cal = Tstamp.makeTimestamp();
    cal.setYear(year);
    cal.setMonth(month);
    cal.setDay(1);
    cal = quantize(cal);
    return cal;
  }
  
  /**
   * Computes the first day of the next month.
   * Zeros the hour.minute.second.millisecond for this timestamp.
   * @param year the number of the year as an integer
   * @param month the number of the month as an integer
   * @return the first of the month timestamp as an XMLGregorianCalendar object
   */
  private XMLGregorianCalendar getFirstOfNextMonth(int year, int month) {
    XMLGregorianCalendar cal = Tstamp.makeTimestamp();
    int nextMonth = ((month % 12) + 1); 
    int nextYear = year;
    if (nextMonth == 1) {
      nextYear++;
    }
    cal.setYear(nextYear);
    cal.setMonth(nextMonth);
    cal.setDay(1);
    cal = quantize(cal);
    return cal;
  }

  /**
   * Return a formatted description of the requested sensor data on the specified interval.
   * @param uri the URI for the SensorBase
   * @param project the project of interest
   * @param user user to authentication
   * @param password user password for authentication
   * @param year of interval
   * @param month of interval
   * @return Boolean array of days with data
   * @throws RPCException if problems occur during the Sensorbase procedure call
   */
  public ArrayList<Boolean> getOverview(String uri, String project, String user, String password, 
    int year, int month) throws RPCException {
    XMLGregorianCalendar start = getFirstOfMonth(year, month);
    XMLGregorianCalendar end = getFirstOfNextMonth(year, month);
    String params = "URI: " + uri + ", Project: " + project + USERLABEL + user 
      + ", Password: " + password + ", year: " +  year + ", month: " + month 
      + ", start: " +  start + ", end: " +  end + '\n';
    log("PV getOverview entering method with params:\n" + params);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    ArrayList<Boolean> result = new ArrayList<Boolean>();
    for (int i = 0; i < 32; i++) {
      result.add(i, Boolean.FALSE);
    }
    try {
      SensorBaseClient client = new SensorBaseClient(uri, user, password);
      SensorDataIndex index = client.getProjectSensorData(user, project, start, end);
      log("PV getOverview: SensorDataIndex retrieved.\n");
      ArrayList<XMLGregorianCalendar> dateslist = new ArrayList<XMLGregorianCalendar>();
      for (SensorDataRef ref : index.getSensorDataRef()) {
        XMLGregorianCalendar cal = quantize(ref.getTimestamp());
        if (!dateslist.contains(cal)) {
          dateslist.add(cal);
        }
      }
      for (XMLGregorianCalendar i : dateslist) {
        result.set(i.getDay(), Boolean.TRUE);
      }
      log("PV getOverview: ArrayList populated with data values.\n");
    } 
    catch (Exception e) {
      log("PV getOverview exception thrown " + e.getMessage() + '\n');
    }
    stopwatch.stop();
    log("PV getOverview exiting method with execution time of " + stopwatch + '\n');
    return result; 
  }

  /**
   * Returns the index of all Projects from this server for this user..
   * @param uri the URI for the SensorBase.
   * @param user username (email address) for authentication.
   * @param password user password for authentication.
   * @return Returns the index of all Projects from this server for this user.
   * @throws RPCException if problems occur during the Sensorbase procedure call.
   */
  public ArrayList<ProjectParcel> getProjectParcels(String uri, String user, String password)
    throws RPCException {
    String params = "URI: " + uri + USERLABEL + user + '\n';
    log("PV getProjectParcels entering method with params:\n" + params);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    ArrayList<ProjectParcel> result = new ArrayList<ProjectParcel>();
    try {
      SensorBaseClient client = new SensorBaseClient(uri, user, password);
      ProjectIndex index = client.getProjectIndex(user);
      for (ProjectRef ref : index.getProjectRef()) {
        Project p = client.getProject(ref);
        ProjectParcel projectParcel = new ProjectParcel();
        projectParcel.setName(p.getName());
        projectParcel.setDescription(p.getDescription());
        projectParcel.setOwner(p.getOwner());
        projectParcel.setMembers(p.getMembers().getMember());
        projectParcel.setInvitations(p.getInvitations().getInvitation());
        HashMap<String, String> propertMap = new HashMap<String, String>();
        for (Property property : p.getProperties().getProperty()) {
          propertMap.put(property.getKey(), property.getValue());
        }
        projectParcel.setProperties(propertMap);
        projectParcel.setUriPatterns(p.getUriPatterns().getUriPattern());
        projectParcel.setStartTime(SimpleDateFactories.getInstance().getDatefactory().format(
          p.getStartTime().toGregorianCalendar().getTime()));
        projectParcel.setEndTime(SimpleDateFactories.getInstance().getDatefactory().format(
          p.getEndTime().toGregorianCalendar().getTime()));
        result.add(projectParcel);
        log("PV getProjectParcels: adding project " + projectParcel.getName());
      }
    } 
    catch (Exception e) {
      log("PV getProjectParcels: exception thrown: " + e.getMessage() + '\n');
    }
    stopwatch.stop();
    log("PV getProjectParcels exiting method with execution time of " 
      + stopwatch + '\n');
    return result; 
  }

  /**
   * Invites the user to the project.
   * @param host the URI for the SensorBase.
   * @param inviter username (email address) for authentication.
   * @param password user password for authentication.
   * @param invitee username (email address) destination for invitation.
   * @param projectName the project name.
   * @return Returns the index of all Projects from this server for this user.
   * @throws RPCException if problems occur during the Sensorbase procedure call.
   */
  public ArrayList<String> invite(String host, String inviter, String password, 
    String invitee, String projectName) throws RPCException {
    String params = "URI: " + host + USERLABEL + inviter + ", Password: " + password + '\n';
    log("PV invite entering method with params:\n" + params);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    ArrayList<String> result = new ArrayList<String>();
    try {
      SensorBaseClient client = new SensorBaseClient(host, inviter, password);
      Project p = client.invite(invitee, projectName);
      
      ArrayList<String> projectInfo = new ArrayList<String>();
      projectInfo.add(p.getName());
      projectInfo.add(p.getDescription());
      projectInfo.add(SimpleDateFactories.getInstance().getDatefactory().format(
        p.getStartTime().toGregorianCalendar().getTime()));
      projectInfo.add(SimpleDateFactories.getInstance().getDatefactory().format(
        p.getEndTime().toGregorianCalendar().getTime()));
      result = projectInfo;
      log("PV invite: ArrayList populated with project info values.\n");
    } 
    catch (Exception e) {
      log("PV invite exception thrown: " + e.getMessage() + '\n');
      // throw new RPCException(e.getMessage()); // NOPMD
    }
    stopwatch.stop();
    log("PV invite exiting method with execution time of " + stopwatch + '\n');
    return result; 
  }

  /**
   * This method responds to an invitation to join a project.
   * @param host the URI for the SensorBase.
   * @param email username (Invitee).
   * @param password user password for authentication.
   * @param projectName the project name.
   * @param owner of project (Inviter).
   * @param reply true to accept, false to decline.
   * @return Returns the index of all Projects from this server for this user.
   * @throws RPCException if problems occur during the Sensorbase procedure call.
   */
  public String rsvp(String host, String email, String password,
    String projectName, String owner, Boolean reply ) throws RPCException {
    String params = "URI: " + host + USERLABEL + email + ", Password: " + password 
      + ", Project: " + projectName + ", Reply: " + reply + '\n';
    log("PV respondToInvitation entering method with params:\n" + params);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
    String result = Boolean.TRUE.toString();
    try {
      SensorBaseClient client = new SensorBaseClient(host, email, password);
      if (reply.equals(Boolean.TRUE)) {
        client.reply(owner, projectName, SensorBaseClient.InvitationReply.ACCEPT);
      }
      else {
        client.reply(owner, projectName, SensorBaseClient.InvitationReply.DECLINE);
      }
    }
    catch (Exception e) {
      result = "HTTP response " + e.getMessage();
      log("PV respondToInvitation exception thrown: " + result + '\n');
    }
    stopwatch.stop();
    log("PV respondToInvitation exiting method with result " + result +
      " and an execution time of " + stopwatch + '\n');
    return result; 
  }
  
  /**
   * Retrieves the sensordata index for this project.
   * 
   * @param host the URI for the SensorBase.
   * @param user user to authentication.
   * @param password user password for authentication.
   * @param project the project name.
   * @return ExtendedProjectData The project data.
   */
  SensorDataIndex getProjectSensorDataIndex(String host, String user, String password, 
      String project) {
    // Create the client and check authentication.
    Date currDate = new Date();
    XMLGregorianCalendar endTime = Tstamp.makeTimestamp(currDate.getTime());
    GregorianCalendar cal = new GregorianCalendar();
    cal.setTime(currDate);
    cal.add(GregorianCalendar.DAY_OF_YEAR, -1);
    XMLGregorianCalendar startTime = Tstamp.makeTimestamp(cal.getTimeInMillis());
    SensorBaseClient client = new SensorBaseClient(host, user, password);
    ArrayList<SensorDataRef> index2send = new ArrayList<SensorDataRef>();
    SensorDataIndex index = null;
    try {
      client.authenticate();
      Project prj = client.getProject(user, project);
      XMLGregorianCalendar lastDate = prj.getStartTime();
      log("calling sensorbase at " + host + " for project " + project
          + " sensor data index " + Tstamp.makeTimestamp(startTime).toString());
      index = client.getProjectSensorData(user, project, startTime, endTime);
      Iterator<SensorDataRef> itr = index.getSensorDataRef().iterator();
      while ((index2send.size() < 50) && (startTime.compare(lastDate) > 0)) {
        if (itr.hasNext()) {
          index2send.add(itr.next());
        }
        else {
          endTime = startTime;
          cal = endTime.toGregorianCalendar();
          cal.add(GregorianCalendar.DAY_OF_YEAR, -1);
          startTime = Tstamp.makeTimestamp(cal.getTimeInMillis());
          index = client.getProjectSensorData(user, project, startTime, endTime);
          itr = index.getSensorDataRef().iterator();
        }
      }
    }
    catch (SensorBaseClientException e) {
      log("exception while retrieving sesnor data index: " + e.getMessage());
    }
    log("ProjectSensorData index obtained.");
    return index;
  }

  /**
   * Put a project.
   * 
   * @param host the URI for the SensorBase.
   * @param user user to authentication.
   * @param password user password for authentication.
   * @param parcel the project data.
   * @return String whether the deletion succeeded.
   */
  public String putProject(String host, String user, String password, ProjectParcel parcel) {
    log("PV: NewProject");
    
    Project project = new Project();
    project.setName(parcel.getName());
    project.setOwner(parcel.getOwner());
    project.setDescription(parcel.getDescription());
    
    Members members = new Members();
    for (Object obj : parcel.getMembers()) {
      if (obj instanceof String) {
        members.getMember().add((String)obj);
      }
    }
    project.setMembers(members);
    
    Invitations invitations = new Invitations();
    for (Object obj : parcel.getInvitations()) {
      if (obj instanceof String) {
        invitations.getInvitation().add((String)obj);
      }
    }
    project.setInvitations(invitations);

    UriPatterns patterns = new UriPatterns();
    for (Object obj : parcel.getUriPatterns()) {
      if (obj instanceof String) {
        patterns.getUriPattern().add((String)obj);
      }
    }
    project.setUriPatterns(patterns);

    Properties prop = new Properties();
    for (Object obj : parcel.getProperties().entrySet()) {
      if (obj instanceof Entry) {
        Object keyObject = ((Entry)obj).getKey();
        Object valueObject = ((Entry)obj).getValue();
        if (keyObject instanceof String && valueObject instanceof String) {
          Property pr = new Property();
          pr.setKey(((String)keyObject));
          pr.setValue(((String)valueObject));
          prop.getProperty().add(pr);
        }
      }
    }
    project.setProperties(prop);

    SensorBaseClient client = new SensorBaseClient(host, user, password);
    try {
      project.setStartTime(Tstamp.makeTimestamp(parcel.getStartTime()));
      project.setEndTime(Tstamp.makeTimestamp(parcel.getEndTime()));
      client.authenticate();
      client.putProject(project);
      log("PV NewProject: Succesfully PUT new project.");
      return Boolean.TRUE.toString();
    }
    catch (Exception e) {
      log("PV NewProject error: " + e.getMessage());
      return e.getMessage();
    }
  }

  /**
   * Deletes project data.
   * 
   * @param host the URI for the SensorBase.
   * @param user user to authentication.
   * @param password user password for authentication.
   * @param project the project name.
   * @return Boolean whether the deletion succeeded.
   */
  public String deleteProject(String host, String user, String password, String project) {
    SensorBaseClient client = new SensorBaseClient(host, user, password);
    try {
      client.authenticate();
      client.deleteProject(user, project);
      return Boolean.TRUE.toString();
    }
    catch (SensorBaseClientException e) {
      log("PV unable to DELETE project " + project + ": " + e.getMessage());
      return e.getMessage();
    }
  }

  /**
   * Logs the error message from the javascript client.
   * 
   * @param text message to log.
   */
  public void logClient(String text) {
    log("PV message from client: " + text + '\n');
  }
}